package com.luych.toolbox.automation.service.job;

import com.luych.toolbox.automation.service.constant.Constant;
import com.luych.toolbox.automation.service.core.connection.SqlConnection;
import com.luych.toolbox.automation.service.core.connection.SshConnection;
import com.luych.toolbox.automation.service.core.executor.BashExecutor;
import com.luych.toolbox.automation.service.core.executor.FileExecutor;
import com.luych.toolbox.automation.service.core.executor.SqlExecutor;
import com.luych.toolbox.automation.service.core.executor.TaskExecutor;
import com.luych.toolbox.automation.service.core.stdout.*;
import com.luych.toolbox.automation.service.dao.*;
import com.luych.toolbox.automation.service.entity.*;
import com.luych.toolbox.common.enums.*;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketEventPublisher;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketLetterPublisher;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketNoticePublisher;
import com.luych.toolbox.common.feign.intf.base.service.ScriptFeign;
import com.luych.toolbox.common.feign.intf.base.service.TaskFeign;
import com.luych.toolbox.common.feign.intf.base.service.TaskLogFeign;
import com.luych.toolbox.common.feign.intf.database.service.DataSourceFeign;
import com.luych.toolbox.common.feign.intf.gateway.center.FileFeign;
import com.luych.toolbox.common.feign.param.base.service.TaskLogParam;
import com.luych.toolbox.common.feign.param.base.service.TaskParam;
import com.luych.toolbox.common.feign.param.gateway.center.WebSocketEvent;
import com.luych.toolbox.common.feign.view.base.service.ScriptArgView;
import com.luych.toolbox.common.feign.view.base.service.ScriptView;
import com.luych.toolbox.common.feign.view.base.service.TaskView;
import com.luych.toolbox.common.feign.view.data.service.DataSourceView;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.transaction.support.TransactionTemplate;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class AutoTaskHistoryExecuteJob extends QuartzJobBean {
    private static final Logger LOGGER = LoggerFactory.getLogger(AutoTaskHistoryExecuteJob.class);

    private final BusProperties busProperties;
    private final ApplicationEventPublisher applicationEventPublisher;

    private final TransactionTemplate transactionTemplate;
    private final AutoTaskHistoryDao historyDao;
    private final AutoTaskHistoryArgDao historyArgDao;
    private final AutoTaskHistoryServerDao historyServerDao;
    private final AutoTaskTemplateDao templateDao;
    private final AutoTaskTemplateArgDao templateArgDao;
    private final AutoTaskTemplateNodeDao templateNodeDao;
    private final AutoTaskTemplateNodeStdinDao templateNodeStdinDao;
    private final AutoTaskTemplateNodeStdoutDao templateNodeStdoutDao;
    private final AutoSshServerDao sshServerDao;
    private final DataSourceFeign dataSourceFeign;
    private final ScriptFeign scriptFeign;
    private final FileFeign fileFeign;
    private final TaskFeign taskFeign;
    private final TaskLogFeign taskLogFeign;

    public AutoTaskHistoryExecuteJob(BusProperties busProperties,
                                     ApplicationEventPublisher applicationEventPublisher,
                                     TransactionTemplate transactionTemplate,
                                     AutoTaskHistoryDao historyDao,
                                     AutoTaskHistoryArgDao historyArgDao,
                                     AutoTaskHistoryServerDao historyServerDao,
                                     AutoTaskTemplateDao templateDao,
                                     AutoTaskTemplateArgDao templateArgDao,
                                     AutoTaskTemplateNodeDao templateNodeDao,
                                     AutoTaskTemplateNodeStdinDao templateNodeStdinDao,
                                     AutoTaskTemplateNodeStdoutDao templateNodeStdoutDao,
                                     AutoSshServerDao sshServerDao,
                                     DataSourceFeign dataSourceFeign,
                                     ScriptFeign scriptFeign,
                                     FileFeign fileFeign,
                                     TaskFeign taskFeign,
                                     TaskLogFeign taskLogFeign) {
        this.busProperties = busProperties;
        this.applicationEventPublisher = applicationEventPublisher;
        this.transactionTemplate = transactionTemplate;
        this.sshServerDao = sshServerDao;
        this.templateDao = templateDao;
        this.historyDao = historyDao;
        this.historyArgDao = historyArgDao;
        this.historyServerDao = historyServerDao;
        this.templateArgDao = templateArgDao;
        this.templateNodeDao = templateNodeDao;
        this.templateNodeStdinDao = templateNodeStdinDao;
        this.templateNodeStdoutDao = templateNodeStdoutDao;
        this.dataSourceFeign = dataSourceFeign;
        this.scriptFeign = scriptFeign;
        this.fileFeign = fileFeign;
        this.taskFeign = taskFeign;
        this.taskLogFeign = taskLogFeign;
    }

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        String id = context.getJobDetail().getJobDataMap().getString("id");
        String datetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());

        WebSocketLetterPublisher letterPublisher = new WebSocketLetterPublisher(busProperties, applicationEventPublisher);
        WebSocketNoticePublisher noticePublisher = new WebSocketNoticePublisher(busProperties, applicationEventPublisher);
        WebSocketEventPublisher eventPublisher = new WebSocketEventPublisher(busProperties, applicationEventPublisher);

        AutoTaskHistory taskHistory = historyDao.findById(id).orElseThrow(() ->
                new JobExecutionException(Constant.HISTORY_NOT_FOUND));
        taskHistory.setStartDate(new Date());
        taskHistory.setStatus(AutoTaskHistoryStatus.RUNNING);
        historyDao.save(taskHistory);

        eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.AUTO_TASK_HISTORY_RUNNING, taskHistory.getId()));

        LOGGER.info("start to exec task [{}]...", taskHistory.getName());
        String message = "自动化任务执行通知";
        try {
            Map<String, String> runtimeArgs = this.initRuntimeArgs(taskHistory.getId());

            AutoTaskTemplate template = templateDao.findById(taskHistory.getAutoTaskTemplateId()).orElseThrow(() ->
                    new JobExecutionException(Constant.TEMPLATE_NOT_FOUND));
            List<AutoTaskTemplateNode> templateNodes = templateNodeDao.findByAutoTaskTemplateId(template.getId())
                    .stream().sorted(Comparator.comparing(AutoTaskTemplateNode::getNum)).collect(Collectors.toList());
            for (AutoTaskTemplateNode templateNode : templateNodes) {
                if (AutoTaskTemplateNodeType.BASH.equals(templateNode.getType()) || AutoTaskTemplateNodeType.SQL.equals(templateNode.getType())) {
                    this.execScript(taskHistory, templateNode, runtimeArgs);
                } else if (AutoTaskTemplateNodeType.FILE.equals(templateNode.getType())) {
                    this.uploadFile(taskHistory, templateNode);
                }
            }
            taskHistory.setCompleteDate(new Date());
            taskHistory.setStatus(AutoTaskHistoryStatus.SUCCESS);
            historyDao.save(taskHistory);

            String content = "自动化任务[" + taskHistory.getName() + "]于" + datetime + "执行成功！";
            letterPublisher.success(taskHistory.getOwnerId(), message, content);
            noticePublisher.success(taskHistory.getOwnerId(), content);
            eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.AUTO_TASK_HISTORY_SUCCESS, taskHistory.getId()));
        } catch (Exception e) {
            LOGGER.info("error to exec task [{}]: {}", taskHistory.getName(), e.getMessage(), e);
            StringWriter writer = new StringWriter();
            e.printStackTrace(new PrintWriter(writer));

            taskHistory.setCompleteDate(new Date());
            taskHistory.setStatus(AutoTaskHistoryStatus.FAILED);
            taskHistory.setErrorMessage(e.getMessage());
            taskHistory.setErrorTrace(writer.toString());
            historyDao.save(taskHistory);

            String content = "数据备份任务[" + taskHistory.getName() + "]于" + datetime + "执行异常，请及时处理。";
            noticePublisher.error(taskHistory.getOwnerId(), content);
            content += "\n\n\n错误信息：\n" + e.getMessage() + "\n\n\n异常堆栈：\n" + writer;
            letterPublisher.error(taskHistory.getOwnerId(), message, content);
            eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.AUTO_TASK_HISTORY_ERROR, taskHistory.getId()));
        }
    }

    private void uploadFile(AutoTaskHistory taskHistory, AutoTaskTemplateNode templateNode) throws JobExecutionException {
        List<AutoTaskHistoryServer> historyServers = historyServerDao.findByAutoTaskHistoryId(taskHistory.getId());
        historyServers.sort(Comparator.comparing(AutoTaskHistoryServer::getServerIndex));
        String sshServerId = historyServers.get(templateNode.getServerIndex()).getAutoSshServerId();
        AutoSshServer sshServer = sshServerDao.findById(sshServerId).orElseThrow(() ->
                new JobExecutionException(Constant.SSH_SERVER_NOT_FOUND));

        Resource fileResource = fileFeign.get(templateNode.getFileId());
        InputStream fileStream;
        try {
            fileStream = fileResource.getInputStream();
        } catch (IOException e) {
            throw new JobExecutionException(e);
        }
        TaskExecutor<SshConnection> taskExecutor = new FileExecutor(fileStream);
        SshConnection sshConnection = new SshConnection(sshServer.getHost(), sshServer.getPort(), sshServer.getUser(), sshServer.getPass());

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.TASK_FILE);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("执行脚本");
        taskParam.setDescription("为服务器[" + sshServer.getName() + "]上传文件[" + templateNode.getFilePath() + "]");
        taskParam.setStdin(templateNode.getFileId());
        taskParam.setOwnerId(taskHistory.getOwnerId());
        TaskView taskResult = taskFeign.create(taskParam);
        taskFeign.start(taskResult.getId());
        try {
            taskExecutor.execute(sshConnection, templateNode.getFilePath());
            taskFeign.complete(taskResult.getId(), TaskStatus.SUCCESS, "成功");
        } catch (Exception e) {
            taskFeign.complete(taskResult.getId(), TaskStatus.ERROR, e.getMessage());
            throw new JobExecutionException(e);
        }
    }

    private void execScript(AutoTaskHistory taskHistory, AutoTaskTemplateNode templateNode, Map<String, String> runtimeArgs) throws JobExecutionException {
        ScriptView script = scriptFeign.get(templateNode.getScriptId());
        List<String> nodeArgs = this.getNodeArgs(templateNode.getId(), script.getArgs(), runtimeArgs);
        String stdout = "";
        if (AutoTaskTemplateNodeType.BASH.equals(templateNode.getType())) {
            List<AutoTaskHistoryServer> historyServers = historyServerDao.findByAutoTaskHistoryId(taskHistory.getId());
            historyServers.sort(Comparator.comparing(AutoTaskHistoryServer::getServerIndex));
            String sshServerId = historyServers.get(templateNode.getServerIndex()).getAutoSshServerId();
            AutoSshServer sshServer = sshServerDao.findById(sshServerId).orElseThrow(() ->
                    new JobExecutionException(Constant.SSH_SERVER_NOT_FOUND));
            stdout = this.execBash(sshServer, script, nodeArgs, taskHistory.getOwnerId());
        } else if (AutoTaskTemplateNodeType.SQL.equals(templateNode.getType())) {
            DataSourceView dataSource = dataSourceFeign.get(templateNode.getDataSourceId());
            String dataSourcePassword = dataSourceFeign.password(templateNode.getDataSourceId());
            stdout = this.execSql(dataSource, dataSourcePassword, script, nodeArgs, taskHistory.getOwnerId());
        }
        List<AutoTaskTemplateNodeStdout> templateNodeStdouts = templateNodeStdoutDao.findByAutoTaskTemplateNodeId(templateNode.getId());
        for (AutoTaskTemplateNodeStdout templateNodeStdout : templateNodeStdouts) {
            StdoutParser stdoutParser = new LineParser();
            if (AutoTaskTemplateNodeType.SQL.equals(templateNode.getType())) {
                stdoutParser = new JacksonJsonParser();
            } else if (AutoTaskTemplateNodeStdoutType.JSON.equals(templateNodeStdout.getType())) {
                stdoutParser = new JacksonJsonParser();
            } else if (AutoTaskTemplateNodeStdoutType.XML.equals(templateNodeStdout.getType())) {
                stdoutParser = new JacksonXmlParser();
            } else if (AutoTaskTemplateNodeStdoutType.YML.equals(templateNodeStdout.getType())) {
                stdoutParser = new JacksonYmlParser();
            } else if (AutoTaskTemplateNodeStdoutType.LINE.equals(templateNodeStdout.getType())) {
                stdoutParser = new LineParser();
            }
            String runtimeValue = stdoutParser.parse(stdout, templateNodeStdout.getExpression());
            runtimeArgs.put(templateNodeStdout.getAutoTaskTemplateArgId(), runtimeValue);
        }
    }

    private Map<String, String> initRuntimeArgs(String historyId) throws JobExecutionException {
        List<AutoTaskHistoryArg> historyArgs = historyArgDao.findByAutoTaskHistoryId(historyId);
        Map<String, String> runtimeArgs = new HashMap<>();
        for (AutoTaskHistoryArg historyArg : historyArgs) {
            AutoTaskTemplateArg templateArg = templateArgDao.findById(historyArg.getAutoTaskTemplateArgId()).orElseThrow(() ->
                    new JobExecutionException(Constant.TEMPLATE_ARG_NOT_FOUND));
            runtimeArgs.put(templateArg.getId(), historyArg.getValue());
        }
        return runtimeArgs;
    }

    private String execSql(DataSourceView dataSource, String dataSourcePassword, ScriptView script, List<String> nodeArgs, String ownerId) throws JobExecutionException {
        TaskExecutor<SqlConnection> taskExecutor = new SqlExecutor(script.getText());
        SqlConnection sqlConnection = new SqlConnection(dataSource.getType(), dataSource.getHost(), dataSource.getPort(), dataSource.getUser(), dataSourcePassword, dataSource.getDb());

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.TASK_SQL);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("执行SQL");
        taskParam.setDescription("在数据源 [" + dataSource.getName() + "]上执行SQL[" + script.getName() + "]");
        taskParam.setStdin("###### 脚本内容 ######" + "\r\n" + script.getText() + "\r\n" + "###### 执行参数 ######" + "\r\n" + String.join("\r\n", nodeArgs));
        taskParam.setOwnerId(ownerId);
        TaskView taskResult = taskFeign.create(taskParam);
        taskFeign.start(taskResult.getId());
        try {
            String stdout = taskExecutor.execute(sqlConnection, nodeArgs.toArray(new String[0]));
            taskFeign.complete(taskResult.getId(), TaskStatus.SUCCESS, stdout);
            return stdout;
        } catch (Exception e) {
            taskFeign.complete(taskResult.getId(), TaskStatus.ERROR, e.getMessage());
            throw new JobExecutionException(e);
        }
    }

    private String execBash(AutoSshServer server, ScriptView script, List<String> nodeArgs, String ownerId) throws JobExecutionException {
        BashExecutor taskExecutor = new BashExecutor(script.getText());
        SshConnection sshConnection = new SshConnection(server.getHost(), server.getPort(), server.getUser(), server.getPass());

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.TASK_BASH);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("执行脚本");
        taskParam.setDescription("在服务器[" + server.getName() + "]上执行脚本[" + script.getName() + "]");
        taskParam.setStdin("###### 脚本内容 ######" + "\r\n" + script.getText() + "\r\n" + "###### 执行参数 ######" + "\r\n" + String.join("\r\n", nodeArgs));
        taskParam.setOwnerId(ownerId);
        TaskView taskView = taskFeign.create(taskParam);
        taskFeign.start(taskView.getId());
        try {
            Consumer<String> std = log -> {
                TaskLogParam taskLogParam = new TaskLogParam();
                taskLogParam.setTaskId(taskView.getId());
                taskLogParam.setLog(log);
                taskLogFeign.create(taskLogParam);
            };
            taskExecutor.setStderr(std);
            taskExecutor.setStdout(std);
            String stdout = taskExecutor.execute(sshConnection, nodeArgs.toArray(new String[0]));
            taskFeign.complete(taskView.getId(), TaskStatus.SUCCESS, stdout);
            return stdout;
        } catch (Exception e) {
            taskFeign.complete(taskView.getId(), TaskStatus.ERROR, e.getMessage());
            throw new JobExecutionException(e);
        }
    }

    private List<String> getNodeArgs(String templateNodeId, List<ScriptArgView> scriptArgs, Map<String, String> runtimeArgs) {
        List<String> nodeArgs = new ArrayList<>();
        scriptArgs.sort(Comparator.comparing(ScriptArgView::getName));
        for (ScriptArgView scriptArg : scriptArgs) {
            AutoTaskTemplateNodeStdin templateNodeStdin = templateNodeStdinDao.getByAutoTaskTemplateNodeIdAndScriptArgId(templateNodeId, scriptArg.getId());
            if (AutoTaskTemplateNodeStdinType.INPUT.equals(templateNodeStdin.getType())) {
                nodeArgs.add(templateNodeStdin.getValue());
            } else if (AutoTaskTemplateNodeStdinType.ARG.equals(templateNodeStdin.getType())) {
                nodeArgs.add(runtimeArgs.get(templateNodeStdin.getAutoTaskTemplateArgId()));
            }
        }
        return nodeArgs;
    }
}
